
\section{我的经验}

\subsection{无从到有——时间点提醒}

印象中我是在2009年5月左右开始使用提醒类软件的，
理由很简单，就是学校生活中有太多琐碎的Deadline和任务安排。
在我几次忘记并错过了作业的Deadline之后，终于认识到我的大脑无法胜任这种细碎的记忆工作。
更何况很多很多的时间点都是过后就没有什么意义的，
记住这种没有价值的事情让我觉得很不值，特别是我并不是一个擅长记忆的人。

当时用的提醒软件叫\underline{雪狐提醒簿}\footnote{主页
\href{http://www.mycaca.com/download/RmdBook-01.jsp}{\emph{传送门}}}，
从功能上来说，这并不是一个复杂的程序，其“唯一”功能就是定时提醒。
你有什么事情需要提醒，那就加入一个“提醒”，到了指定的时候它就会跳出来告诉你有这么一个提醒。
另外它还有基本的、也是常见的各种周期重复提醒功能。

这个程序基本满足了我当时的需求，虽然有各种不爽，但仍然使用了一年，
而且购买了正版。

这个程序是一个典型的\underline{时间点提醒软件}，
即只能指定一个提醒时间，没有任务和任务长度的概念。
它有如下的一些问题：

\subsubsection{特殊的日历和重复}
\label{sec:myexp.school}

这个需求其实很奇怪，我到现在也未能发现太多的重复模式，但学生的日程是很复杂的，
因为学生有\underline{校历}的概念。
校历一般是以周为单位，从开学第一周开始每周都被编号，直到期末考试完，
很多的时间都是以此为基准的。
一般来看这并不是一个很大的问题，因为只要在添加提醒时把校历时间手工转换为公历时间就行了。

但有两个问题无法克服，一是时间的修改。比如说某个在校历第8周周三的任务被改到了第7周周五，
如果不是对日期特别清楚的话就需要手工对照日历来修改，而当现在距第8周较远时弄清哪周是第8周、
哪周是第7周又成为一个小问题，在这些细碎的工作中，错误就可能出现。
第二个问题是基于校历的特殊重复任务就很难表述，
例如很多课程是1-16周，有些课是诸如2-7周之类，还有些课是1-16周的单周或是双周。
这些都还好，实验课往往是不规律的，如某门课是1-3,5-6,8,11,13周上。

这些重复模式应该如何添加在传统提醒系统中呢？
把不规律的重复提醒展开为单次提醒？但可能每次上课需要多个提醒，如准备上课资料、复习、作业等各个细碎的提醒，
手工展开是没有办法的办法，编码中有DRY原则，在日程管理系统中其实也有DRY原则。
这样当这门课的上课时间整体由周二移到周五时，你就不会感觉特别不爽了。

\subsubsection{提醒无法表示预约}

其实一般情况下只有时间点提醒的程序可以模拟“预约”概念，
只需要把一个预约分解为多个时间点提醒就可以了。
根据我的经验，一般只需添加一个“预约开始”提醒就行了，
重要预约可以再添加一个“下一步行动预提醒”。
具体的情况可以留到预约开始时再添加新提醒来完成，当然，这并非好习惯。

而问题的关键在于，时间点程序无法判断两个预约是否会冲突，
毕竟它就没有预约的概念，一切东西都是长度为0的时间点，
除非你在添加预约的时候查看下目标时间范围内是否有其他预约，
否则你迟早会发现日程表上有两个预约重叠了。

这里八卦一下，其实在时间提醒程序中可以加一个小补丁来改善这方面的用户体验，
那就是在添加提醒的时候显示目标时间附近的提醒。但我从未见到这样的程序，
当然那些只能在日历上添加提醒的程序
除外\footnote{想象一下：假设那天有20个提醒，但日历窗口上显示不下这么多提醒时的情况。}。

最好的解决办法就是引入预约的概念，预约定义为：
在固定时间开始，有时间长度的项。它占据着日程表上的一块空间。

\subsubsection{提醒也无法表示任务}

任务是一个更麻烦的概念，指的是某件需要完成的事情，一般都限制在要在某时刻之前完成，
也可以加入其他时间限制，但唯一不需要指定的是这个任务必须要在什么时候开始做……
这对于以时间作为提醒条件的提醒程序简直是最大的“反例”。

而且用提醒来实现任务还有诸多问题，例如会导致提醒的频繁推迟，以至于成了提醒系统中主要的提醒添加来源……
由于\emph{时间点程序的用户对于将来的日程并没有特别明确的感觉，
在添加新提醒时也往往没法确定适合的具体时刻，一般就是随便指定一个不合其他提醒重合时间。
而这个时间往往是不适合的，在这个时间到来时只是提醒了用户该做这个事情，
但用户当时可能正在进行其他不应被打断的任务，这时唯一的选择就是推迟。}
相对来讲比较好的解决办法是在每天的开始浏览下所有提醒，重新调整所有提醒的时间点。
但偏偏这类软件没有比较好的修改时间点的方式，因为它们不是那种复杂的任务程序，
不能指望那种在日历上拖动任务块（在这里提醒其实是一条线）的功能，
所以这个预处理工作比较烦人，总之我当时是不干的（其实也没想到）。

而且对于时间点提醒程序来说，把两个提醒安排在同一个时间是非常糟糕的事情，
这意味这到时候会一下弹出两个窗口，告诉你要同时做两件不同的事情，
这之后一般的情况就是推迟除了一个提醒以外的其他提醒，或者推迟全部提醒，
总之当时我一般最常点的按键就是“推迟5分钟”，然后5分钟后又有一堆窗口弹出来，
用户体验其实很差。而且不光如此，在新建提醒的时候人类总是喜欢制定一个整时刻，
但整时刻其实是很少的，所以冲突在所难免，为了避免这种情况，我后来添加提醒时总是人工产生一个随机时间，
但你可以想象，这个体验也很差。

问题还在继续，当我们推迟提醒的时候往往是手头有其他工作或是状态不佳的时候，
这时候要选择推迟多长时间就已经是一个让人厌烦的事情了，当你估算了一下大概推迟2个小时比较好之后，
你还想研究下两个小时之后是否有其他任务么？
而且当日程安排很紧的时候，你可能需要尝试多个时间才能找到一个空白的时间来安放这个新提醒，
这种感觉听起来就已经很糟了，你不会想要尝试的，特别是在软件界面很简单，没有考虑到这种人性化设置的时候。

\subsubsection{无法避免担惊受怕}

使用时间点提醒仍然无法避免担惊受怕，
原因是你即使把任务作为提醒添加进了提醒系统，你仍然对于未来的时间表没有什么概念，
前面已经说了，任务是否冲突需要自己检查。而且更严重的问题是，除非你仔细分析每个任务和提醒，
否则你没法知道未来的一周是很闲还是很紧还是任务已经多到根本没法做完了。
当然这个问题对于常见的任务程序也存在，所以才提出了第 \ref{sec:function-2ndlevel} 节所说的第二阶段功能。

\subsubsection*{终结}
在使用时间点程序差不多正好一年后，我无法忍受 \ref{sec:myexp.school} 节中所述的问题和
恼人的注册提醒，所以就自己山寨了一个时间点提醒程序，
其特色功能是极其强大的重复时间描述能力。


\subsection{MyReminderV3——重复事件}
\label{sec:myexp.mr.repeat}

八卦一下，MyReminderV3是我个人开发的、自用的、时间点提醒程序，
V1、V2版是什么样的已经不可考，V3版是一个长期使用的版本，
大概使用了一年，其主要特点参见 \ref{sec:myexp.mr.repeat-gen} 节和
\ref{sec:myexp.mr.timelang} 节。

\subsubsection{重复事件}
重复提醒、重复预约、重复任务在日程系统中都是很常见的需求，
将来我们还将讨论重复任务组等等复杂的模型，这里把它们统称为重复事件。
这些重复事件有一个共同的特点：
那就是它们都将以某种规律重复地产生新的日程项，当然这个过程是自动的。
这避免了用户频繁地在传统纸质日程表上抄写重复任务，更主要的是，批量修改这些重复任务也变得非常方便。

而重复事件的重复模式是多种多样的，常见的包括每天、每周、每月、每年以及它们的各种变体。
重复事件还可以分为两类，一是“固定的”，即一次提醒、预约、任务没有完成不会影响它们下一次的时间；
相反就是“不固定的”，其典型例子是采购-缓存类的任务，比如有人每2天洗一次澡，
他把这个活动添加为一个重复事件，周期是2天。
但某一天该洗澡时他因为其他原因而错过了，只好第二天再洗，
那么问题来了，他下一次洗澡是应该在第三天还是第四天呢？
一个较为合理的回答是顺延一天，即直到上一次事件完成后我们才知道下一次事件的准确时间。
对于这种不固定的重复，我们在未来的日程表上只能显示一个虚事件，
直到我们能确定它的时间。
需要说明的是，这种不固定的任务一般都是由用户自己的情况产生的，
不需要和其他人进行交互。


\subsubsection{重复事件发生器}
\label{sec:myexp.mr.repeat-gen}

重复事件的常见的解决方案是，把重复事件作为和单次事件不同的东西来处理，
其差别在于每个重复事件完成后并不会消失，而是显示为下一次的状态。
在日程表上来看就是多了一族事件，只不过这些事件在逻辑上对应到一个重复事件上。

问题就来了，当我们需要临时修改某次事件而又不想影响其他事件时……我们蛋疼了。
一个可行的做法是，在同样的时间位置添加一个新的事件。
一般来说少有支持从重复事件中修改单次事件的系统。
Google日历支持该功能。

为了解决上面这种问题，并尽量兼容各种重复模式，
我在MyReminderV3中引入了\underline{重复事件发生器}，
顾名思义，重复事件发生器表示一个重复事件族，
可以对它进行“展开”操作，按顺序展开出单个事件。

如果采用发生器，修改单个事件很容易，
因为最终处理的还是单个事件，只要把该事件展开出来即可。
但也有不少问题，首先如果发生器按时间顺序展开事件\footnote{这样在技术上较容易实现。}，
那么要修改某个事件就需要把它之前的同类事件都展开出来，
不过根据经验这个问题并不算太大，
根据经验\emph{用户一般要修改的是下个事件或是下面连续的几个事件，很少跳跃}。
第二个问题是已经展开的事件和发生器之间不再有明显的关系\footnote{一个简化的实现是没有关系。}，
使得修改重复事件族的时候需要对发生器和事件分开处理。
为了减少上面两个问题的影响，一般发生器保持展开的自由事件数量为1的状态。

对于未展开的事件，在“日程表显示”或“查看未来某天”的时候通过影子事件来占位，
实质上是分离了显示模型和表示模型。

引入了事件发生器这个中间层后，复杂的重复模式、固定或不固定的重复的处理、
甚至是通过获取系统外信息来决定是否推迟或取消某个事件等等复杂的功能都可以实现。

不过MyReminderV3是定位给程序员的日程提醒系统，它只有一种事件发生器，
这种发生器在创建时由用户输入时间描述代码，并通过惰性求值的方式逐个计算子事件的时间，
这种时间描述代码称作\underline{时间点序列描述语言}。

\subsubsection{时间点序列描述语言}
\label{sec:myexp.mr.timelang}

MyReminderV3是一个时间点提醒程序，为了产生重复提醒，
只需要一个时间点序列就行了。如果是预约或是任务，就需要同时包含其他的参数。

当初还曾想仔细设计这个DSL\footnote{Domain-specific language}，
后来还是嫌麻烦了，最后采用的是嵌入C\#代码。为了简化输入，预定义了大量
函数和常量\footnote{利用了C\#的静态类型特点，把各类时间量如月份、日期等都分别定义了枚举类型，
减少输入错误。}，并且利用了C\#的LINQ和迭代器生成功能，
相对来说还是比较适合处理时间序列的。

这里对这种自用的语言不打算展开介绍，只是举例说明下其描述能力。
用户输入一个函数体，这个函数的声明和其他部分由系统自动加入，
其返回值类型为IEnumerable<DateTime>。

那么一个典型的生日提醒是：
\begin{lstlisting}
return DayInYear(Month1, Day26);
\end{lstlisting}
其中DayInYear返回每年中指定的某一天0点的序列。

农历也是类似的
\begin{lstlisting}
return DayInChineseYear(Month12, Day18);
\end{lstlisting}

时间序列可以做偏移，如
\begin{lstlisting}
return DayInChineseYear(Month1, Day15).AddHours(9);
\end{lstlisting}

对于Thanksgiving Day这样的
\begin{lstlisting}
return WeekDayInMonth(Month11, 4, WeekDay4);
\end{lstlisting}

在某些月份的1号需要调整手表的日期
\begin{lstlisting}
return MonthsLessThan31().NextMonth().AddHours(1);
\end{lstlisting}

由于添加了大量辅助函数，所以增加校历这样的功能是很容易的，如
\begin{lstlisting}
return SchoolOddWeek();
\end{lstlisting}
和不那么连续的
\begin{lstlisting}
return SchoolWeek(1,3,7,8,9).GetDayInWeek(WeekDay3);
\end{lstlisting}
其中的校历基准周是另外输入的。

最能展示其表达能力的描述是这样的
\begin{lstlisting}
var spring = DayInChineseYear(Month1, Day1);
var monthbefore = spring.PreviousMonth()
                        .GetDayInMonth(Day25);
var month = spring.GetDayInMonth(Day25);
return Union(monthbefore, month);
\end{lstlisting}
其功能是返回春节前一月和春节当月的25号，
典型需求是提醒为春节大量群发短信而调整短信包月条数\footnote{不过现在我已经不再干这种无聊的事了。}。
需要说明的是这里的时间序列是有序的，所以Union函数是单独编写的有序的合并。
而且需要注意的是这里的序列经常是无穷序列，不能使用LINQ的排序功能。

其实这里输入的是实际的C\#函数体代码，可以是任何形式，
像使用流程控制语句，使用yield return，使用LINQ中的Where、Select等语句，
使用闭包都是可行的，甚至可以读取外部文件，访问网络，运行外部程序。

但这个函数的目的是为了计算序列的时间，函数应该是纯函数（对日程系统来说），
而且由于缓存的存在，函数的调用时机也是不确定的。
当然也可以引入其他类似的用户输入代码来实现其他功能，
只不过暂时还没有这种必要。

当然这个设计是很烂的，要求用户必须能够熟练使用C\#，
在这里介绍这种方式只是为了启发大家的思路。


\subsubsection{分类、优先级和琐事}

MyReminderV3在设计的时候不光考虑了重复事件问题，也还增加了一些常见功能，
有些是在使用过程中加进去的，这里说的就是分类，优先级。

分类这个功能用处其实不大，文件夹是要在文件多的时候才有用的。
如果提醒很少，那么单纯弄一个分类在查看当天提醒时没啥意义，
其主要作用是在所有提醒中寻找某个提醒时，分类可以作为过滤条件。

优先级这个东西在MyReminderV3中用的很少，因为我的UI做得太烂了，
重要和一般在显示的时候也没有做什么区别。不过后来因为每天列表上的提醒太多，
我又增加了一个优先级叫“琐事”，这类提醒默认是不在列表上显示的，
只是到了时间就跳出来一个提醒。不得不说任务列表上的任务太多也会明显地影响心情，
后面也会提到，显示下一步任务时，显示3个一般就够了，
多了往往会让人感觉怎么还有这么多事情要做。


\subsubsection{题外话——流产的MyReminderV4}

大概在2010年底，我放弃了对MyReminderV3升级的想法\footnote{说停止维护并不恰当，
因为其实已经几乎没有Bug了，而且出现Bug时还是要去修的，只要我还在用这个程序。}，准备开发MyReminderV4，
为此调研了GTD理论，初步形成了本文后面的全部内容的理论基础。

在2011年春节时，在家里尝试开发MyReminderV4的内核，
这个版本的功能是极端激进的（参考第 \ref{sec:function-2ndlevel} 节及
第 \ref{sec:function-3rd-detail-schedule} 节），
在现在看来也是，无论是对开发人员、用户、硬件的计算能力都是极大的挑战，
估计可能未来10年之内也未必会出现实践其观点的软件。


这样的东西毫无疑问地失败了，当初放弃的理由是因为当时的演示内核的效率太低了，
低到假设充分优化后效率能提高3个数量级后仍然不能实用，
当初就是因为这个理由停止了开发。后来才想到，不光是内核的问题，
用户是否需要、能够理解并适应这个新日程系统都是未知的，
而且现在看来结论似乎是否定的，因为它太过于激进了，虽然它将来的用户就是我自己。
但在第 \ref{sec:function-2ndlevel} 节及
第 \ref{sec:function-3rd-detail-schedule} 节仍然会介绍它的设计思想。

所以我转而尝试使用其他日程管理系统，思考用户需求，沉淀想法，
后来就转向了Toodledo。

\subsection{Toodledo}

使用Toodledo并没有特别重要的理由，只是我当时已经是大四下，专心做毕业设计。
日程表上除了导师的组会以外一片空白，提醒系统对我没有什么大用了。
只是还开在那提醒各类生日和节假日等信息，还有几十分钟之后的临时提醒等，
毕竟它还是一个时间点提醒程序，优势还是有的。
除此以外的全部工作都转移到了Toodledo上。

选择Toodledo的理由是，它是一个在线服务，适合我当时有多台电脑的情况，
而且是免费的，还不是一个日程表类的系统。当时也尝试了其他在线服务，
不过阴差阳错还是选择了这个，应该说实在是没有什么重要的理由，
所以像我这样的用户的粘度很低。
事实上Toodledo连一个提醒服务都不是，总之我没发现它有什么好用的客户端，
我的使用方式就是一天跑上去刷几十遍，在某种程度上这是一种倒退……

不过Toodledo并不是一个完全平凡的服务，这里介绍一下它的特点和我使用中发现的问题。

\subsubsection{自动优先级}
Toodledo最有特色的功能应该算是自动优先级了，其实现也并不复杂，
首先由用户指定静态优先级（共5级），系统在显示时会根据任务距其Deadline的时间
再算一个动态优先级（不显示），合成最终优先级，并可以以此在界面上排序，
最终的优先级共9级。

这个功能融合了任务的重要性和紧急程度，算是四象限法\footnote{%
可惜Wiki上没有这个页面，\href{http://en.wikipedia.org/wiki/Time_management}{\textbf{这个}}%
页面提到了一些，不过似乎没发现什么权威的页面，不了解的话还是自己Google吧。}的量化版。
其实现并不复杂，但对用户体验是极大的改善，虽然可定制程度很低，
但相比没有已经是很大的进步。想要获得更好的效果的话大概需要再投入至少10倍的工作量，
而且还会给用户理解造成负担。
在某种程度上这个方法算是“业界最佳实践”，哈哈。

\subsubsection{收费服务}
日程这类的服务最后还是搞成基本功能免费，高级功能收费的模式才是王道。
下面选择其中重要的几个进行介绍。

\paragraph{子任务}
子任务，顾名思义就是在任务下面再尽心划分，现在支持这个的服务不太多，
而且用户需求到底是怎么样的我没长期使用过也不好说。
但不支持子任务肯定是不爽的，但要是层数太多的话，找起来也很麻烦。
后面 \ref{sec:function-1stlevel.subtask} 节将会专门讨论子任务。

\paragraph{数据的可视化}
这个完全是针对用户自己回顾的功能，但是单纯弄个各项任务占时间比例什么的也太水了，属于“糊弄”用户的。

由于这方面行业中还属于空白的状态，我个人也没有什么使用经验，
估计在未来相当长时间内都是系统设计者个人的天地了，其实这个还是蛮考验对用户需求的挖掘和数据可视化的能力的。

\paragraph{推荐下一步工作（Scheduler）}

这个算是最接近第 \ref{sec:function-2ndlevel} 节提到的第二阶段功能的一个实现了，
其功能是：当用户感觉比较闲，无聊的时候，由系统从当前未完成的任务当中推荐一个给用户，
而不是用户自己从任务列表中挑一个，注意这两个的差别是很大的。
这个话题的详细讨论放在第 \ref{sec:function-2ndlevel} 节进行。

Toodledo中的具体实现是：用户输入一个空闲时间，由系统根据该时间和当前日程推荐一个任务给用户。


\subsection{关于日程表类的程序}
由于我没有长时间使用过日程表类的程序，如Google日历之类的，
所以对用户使用的感觉不如前面那样了解，以下讨论都是针对假想用户的。

日程表程序相比时间点程序来讲，最大的进步应该说是引入了预约的概念，
这里的预约指有固定开始时间、固定长度的安排，这种安排一般是排他性的，
例如开会、上课\footnote{和我一样上课不听课的人不在讨论范围内。}、
饭局等。日程表程序的这种特点，使得它特别适合所有事情都是预约的人，
但其他人用起来就会有些问题。

\subsubsection{任务与Deadline}
导致这些问题的原因就是，在日程表程序中，一般只有预约，这和时间点程序是一样的，
\emph{大家只关注自己擅长的地方，对于生活中的实际复杂度则不怎么关心}。
首先缺乏的就是任务，任务在这里指有固定时间长度的工作，但这个工作没有开始时间，
应该在我们有空的时候开始做，这些任务有些有Deadline，而有些没有明确的Deadline，
情况和需求很复杂。貌似有些日程表程序已经支持这种概念，把任务单列出来放在旁边，
看似解决了，但其实还有一个隐藏的问题，那就是除了预约以外，我们仍然不知道实际的日程安排。

说不知道实际日程安排不太准确，因为我们自己就没安排，而现在日程管理系统都是非智能的，
不能指望它们给我们自动安排。实际的问题是：我们不知道\emph{是否存在一个可行的日程安排}。
这句话在一般人看来也很奇怪，什么时候不存在一个可行的日程安排呢？
一个典型的情况是，\emph{我们无法在所有任务各自的Deadline之前完成它们}，
即任务太多了，更准确的说是太密集了。

一旦发现这种问题之后，理性人肯定会把它加入日程系统的必备特性集之中，
因为如果软件不能在任务过多时警告我们，我们就无法完全依赖日程系统，
或者在某天面对必须熬夜加班的情况，甚至有时候加班也不能解决问题。

日程可行性判定是一个复杂的数学问题，
在引入更复杂的模型之后，连如何描述“最优”安排也成了一个
问题，常见的“最优”定义可以参见 \ref{sec:function-2ndlevel.nextstep} 节。
详细的讨论放在第 \ref{sec:function-2ndlevel} 节进行。

\subsubsection{不固定的重复预约}

很遗憾的是对于单纯的日程表程序，一般都不会支持不固定的重复预约，
毕竟在日程表上显示一个虚的任务似乎比较奇怪，
到现在为止\footnote{2011年11月}我尚未见到采用概率模型的日程管理系统。

有些混合的日程系统支持这种功能，如Toodledo。

